@rubytech/create-realagent 1.0.678 → 1.0.681
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +232 -39
- package/package.json +1 -1
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate.test.d.ts +2 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate.test.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate.test.js +112 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate.test.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cache.test.d.ts +2 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cache.test.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cache.test.js +163 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cache.test.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts +38 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.js +130 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/index.js +201 -45
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts +78 -0
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js +194 -0
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js.map +1 -0
- package/payload/platform/lib/graph-mcp/src/__tests__/cypher-validate.test.ts +141 -0
- package/payload/platform/lib/graph-mcp/src/__tests__/schema-cache.test.ts +169 -0
- package/payload/platform/lib/graph-mcp/src/cypher-validate.ts +157 -0
- package/payload/platform/lib/graph-mcp/src/index.ts +247 -47
- package/payload/platform/lib/graph-mcp/src/schema-cache.ts +212 -0
- package/payload/platform/lib/graph-trash/dist/index.d.ts +8 -0
- package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-trash/dist/index.js +109 -14
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-trash/src/index.ts +136 -21
- package/payload/platform/plugins/docs/references/deployment.md +4 -2
- package/payload/platform/plugins/docs/references/memory-guide.md +5 -1
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +20 -0
- package/payload/platform/plugins/memory/PLUGIN.md +1 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +54 -6
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/filter-token.d.ts +36 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/filter-token.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/filter-token.js +86 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/filter-token.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +23 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +47 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.d.ts +58 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.js +125 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-find-candidates.js.map +1 -0
- package/payload/platform/scripts/vnc.sh +12 -409
- package/payload/platform/templates/agents/admin/IDENTITY.md +16 -0
- package/payload/platform/templates/dotfiles/.tmux.conf +1 -0
- package/payload/platform/templates/systemd/maxy-ttyd.service +25 -0
- package/payload/server/chunk-3RBKKDHC.js +783 -0
- package/payload/server/maxy-edge.js +377 -8
- package/payload/server/public/assets/admin-CIkyOur7.js +362 -0
- package/payload/server/public/assets/admin-kHJ-D0s7.css +1 -0
- package/payload/server/public/index.html +2 -1
- package/payload/server/server.js +391 -412
- package/payload/server/public/assets/admin-BBL1no_g.js +0 -352
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, readdirSync, appendFileSync, openSync, closeSync, chmodSync, symlinkSync, unlinkSync, lstatSync, readlinkSync, accessSync, constants as fsConstants } from "node:fs";
|
|
4
4
|
import { resolve, join, dirname } from "node:path";
|
|
5
|
-
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
6
|
+
import { TTYD_VERSION, TTYD_SHA256_BY_ARCH, mapUnameToTtydArch, ttydDownloadUrl, } from "./pinned-binaries.js";
|
|
6
7
|
const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
|
|
7
8
|
// Brand manifest — read from payload to derive all brand-specific installation values.
|
|
8
9
|
// The bundler stamps brand.json into the payload at build time.
|
|
@@ -332,6 +333,7 @@ function pkgsMissing(pkgs) {
|
|
|
332
333
|
function installAptGroup(label, pkgs) {
|
|
333
334
|
const pairs = pkgs.map((original) => ({ original, resolved: resolveAptName(original) }));
|
|
334
335
|
logFile(` apt install (${label}): ${pairs.map((x) => x.resolved).join(" ")}`);
|
|
336
|
+
console.log(" [privileged] apt-get install");
|
|
335
337
|
shell("apt-get", ["install", "-y", ...pairs.map((x) => x.resolved)], { sudo: true });
|
|
336
338
|
const stillMissing = pairs.filter(({ resolved }) => {
|
|
337
339
|
const r = spawnSync("dpkg", ["-s", resolved], { stdio: "pipe", timeout: 5_000 });
|
|
@@ -364,8 +366,12 @@ function installSystemDeps() {
|
|
|
364
366
|
// assertion in vnc.sh check_window_on_display, closing the silent-fail
|
|
365
367
|
// class where PID is alive but no window is mapped on the target display.
|
|
366
368
|
const VNC_DEPS = ["tigervnc-standalone-server", "python3-websockify", "novnc", "xdg-utils", "chromium", "xterm", "xdotool"];
|
|
369
|
+
// Task 657: tmux powers the byte-stream admin terminal. ttyd attaches the
|
|
370
|
+
// shared `maxy-pty` tmux session, so scrollback survives WS reconnects and
|
|
371
|
+
// the same session is reused by the header overlay + upgrade modal.
|
|
372
|
+
const TERMINAL_DEPS = ["tmux"];
|
|
367
373
|
const WIFI_DEPS = ["hostapd", "dnsmasq"];
|
|
368
|
-
const ALL_APT_DEPS = [...BASE_DEPS, ...VNC_DEPS, ...WIFI_DEPS];
|
|
374
|
+
const ALL_APT_DEPS = [...BASE_DEPS, ...VNC_DEPS, ...TERMINAL_DEPS, ...WIFI_DEPS];
|
|
369
375
|
// Task 634 — verify the "deps are present" assumption with `dpkg -s` instead
|
|
370
376
|
// of asserting it (feedback_loud_failures.md). The previous silent-skip
|
|
371
377
|
// branch was benign until Task 632 added xdotool (the first new apt dep
|
|
@@ -388,6 +394,7 @@ function installSystemDeps() {
|
|
|
388
394
|
}
|
|
389
395
|
console.log(` Missing apt deps (${missing.length}): ${missing.join(", ")}`);
|
|
390
396
|
console.log(` Installing via sudo apt-get — sudo may prompt for your password...`);
|
|
397
|
+
console.log(" [privileged] apt-get update");
|
|
391
398
|
shell("apt-get", ["update"], { sudo: true });
|
|
392
399
|
installAptGroup("base utilities", BASE_DEPS);
|
|
393
400
|
installAptGroup("VNC stack", VNC_DEPS);
|
|
@@ -402,9 +409,12 @@ function installSystemDeps() {
|
|
|
402
409
|
// --hostname flag: set unconditionally, no detection, no preservation logic.
|
|
403
410
|
console.log(` Hostname: ${HOSTNAME_FLAG} (from --hostname flag)`);
|
|
404
411
|
try {
|
|
412
|
+
console.log(" [privileged] hostnamectl set-hostname");
|
|
405
413
|
shell("hostnamectl", ["set-hostname", HOSTNAME_FLAG], { sudo: true });
|
|
414
|
+
console.log(" [privileged] sed -i");
|
|
406
415
|
shell("sed", ["-i", `s/127\\.0\\.1\\.1.*$/127.0.1.1\\t${HOSTNAME_FLAG}/`, "/etc/hosts"], { sudo: true });
|
|
407
416
|
try {
|
|
417
|
+
console.log(" [privileged] sed -i");
|
|
408
418
|
shell("sed", ["-i", `s/^[#]*host-name=.*/host-name=${HOSTNAME_FLAG}/`, "/etc/avahi/avahi-daemon.conf"], { sudo: true });
|
|
409
419
|
console.log(` Avahi host-name: ${HOSTNAME_FLAG} (updated avahi-daemon.conf)`);
|
|
410
420
|
}
|
|
@@ -455,9 +465,12 @@ function installSystemDeps() {
|
|
|
455
465
|
console.log(` Hostname: ${BRAND.hostname} (${reason})`);
|
|
456
466
|
hostnameSetAttempted = true;
|
|
457
467
|
try {
|
|
468
|
+
console.log(" [privileged] hostnamectl set-hostname");
|
|
458
469
|
shell("hostnamectl", ["set-hostname", BRAND.hostname], { sudo: true });
|
|
470
|
+
console.log(" [privileged] sed -i");
|
|
459
471
|
shell("sed", ["-i", `s/127\\.0\\.1\\.1.*$/127.0.1.1\\t${BRAND.hostname}/`, "/etc/hosts"], { sudo: true });
|
|
460
472
|
try {
|
|
473
|
+
console.log(" [privileged] sed -i");
|
|
461
474
|
shell("sed", ["-i", `s/^[#]*host-name=.*/host-name=${BRAND.hostname}/`, "/etc/avahi/avahi-daemon.conf"], { sudo: true });
|
|
462
475
|
console.log(` Avahi host-name: ${BRAND.hostname} (updated avahi-daemon.conf)`);
|
|
463
476
|
}
|
|
@@ -500,8 +513,11 @@ function installSystemDeps() {
|
|
|
500
513
|
const avahiDestPath = `/etc/avahi/services/${BRAND.hostname}.service`;
|
|
501
514
|
try {
|
|
502
515
|
writeFileSync(avahiTmpPath, avahiService);
|
|
516
|
+
console.log(" [privileged] cp");
|
|
503
517
|
shell("cp", [avahiTmpPath, avahiDestPath], { sudo: true });
|
|
518
|
+
console.log(" [privileged] systemctl enable");
|
|
504
519
|
shell("systemctl", ["enable", "avahi-daemon"], { sudo: true });
|
|
520
|
+
console.log(" [privileged] systemctl restart");
|
|
505
521
|
shell("systemctl", ["restart", "avahi-daemon"], { sudo: true });
|
|
506
522
|
}
|
|
507
523
|
catch { /* not critical */ }
|
|
@@ -532,6 +548,7 @@ function installSystemDeps() {
|
|
|
532
548
|
if (existsSync("/usr/bin/nmcli") && !existsSync(nmConfFile)) {
|
|
533
549
|
console.log(" Disabling WiFi power save...");
|
|
534
550
|
writeFileSync(`/tmp/${BRAND.hostname}-no-powersave.conf`, "[connection]\nwifi.powersave = 2\n");
|
|
551
|
+
console.log(" [privileged] cp");
|
|
535
552
|
shell("cp", [`/tmp/${BRAND.hostname}-no-powersave.conf`, nmConfFile], { sudo: true });
|
|
536
553
|
spawnSync("sudo", ["systemctl", "restart", "NetworkManager"], { stdio: "pipe" });
|
|
537
554
|
}
|
|
@@ -549,6 +566,7 @@ function installNodejs() {
|
|
|
549
566
|
throw new Error("Automatic Node.js installation is only supported on Linux. Install Node.js 20+ manually.");
|
|
550
567
|
}
|
|
551
568
|
spawnSync("bash", ["-c", "curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -"], { stdio: "inherit" });
|
|
569
|
+
console.log(" [privileged] apt-get install");
|
|
552
570
|
shell("apt-get", ["install", "-y", "nodejs"], { sudo: true });
|
|
553
571
|
}
|
|
554
572
|
function installClaudeCode() {
|
|
@@ -592,6 +610,7 @@ function installClaudeCode() {
|
|
|
592
610
|
}
|
|
593
611
|
else {
|
|
594
612
|
console.log(" This may take 15–30 minutes on Raspberry Pi...");
|
|
613
|
+
console.log(" [privileged] npm install -g @anthropic-ai/claude-code@latest");
|
|
595
614
|
shellRetry("npm", ["install", "-g", ...NPM_NET_FLAGS, "--loglevel", "verbose", "@anthropic-ai/claude-code@latest"], { sudo: true, timeout: 2_400_000 }, // 40 min — Pi downloads can take 25+ min
|
|
596
615
|
3, 30);
|
|
597
616
|
}
|
|
@@ -668,8 +687,10 @@ function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j") {
|
|
|
668
687
|
});
|
|
669
688
|
}
|
|
670
689
|
else {
|
|
690
|
+
console.log(" [privileged] neo4j-admin dbms");
|
|
671
691
|
shell("neo4j-admin", ["dbms", "set-initial-password", "--", password], { sudo: true });
|
|
672
692
|
}
|
|
693
|
+
console.log(" [privileged] systemctl start");
|
|
673
694
|
shell("systemctl", ["start", serviceName], { sudo: true });
|
|
674
695
|
console.log(" Waiting for Neo4j to start...");
|
|
675
696
|
for (let i = 0; i < 15; i++) {
|
|
@@ -797,11 +818,15 @@ function installNeo4j() {
|
|
|
797
818
|
const has17 = policyResult.status === 0 && !policyOutput.includes("Candidate: (none)");
|
|
798
819
|
const javaPackage = has17 ? "openjdk-17-jre-headless" : "openjdk-21-jre-headless";
|
|
799
820
|
console.log(` Installing Java (${javaPackage})...`);
|
|
821
|
+
console.log(" [privileged] apt-get install");
|
|
800
822
|
shell("apt-get", ["install", "-y", javaPackage], { sudo: true });
|
|
801
823
|
spawnSync("bash", ["-c", "curl -fsSL https://debian.neo4j.com/neotechnology.gpg.key | sudo gpg --yes --dearmor -o /usr/share/keyrings/neo4j.gpg 2>/dev/null"], { stdio: "inherit" });
|
|
802
824
|
spawnSync("bash", ["-c", 'echo "deb [signed-by=/usr/share/keyrings/neo4j.gpg] https://debian.neo4j.com stable 5" | sudo tee /etc/apt/sources.list.d/neo4j.list'], { stdio: "inherit" });
|
|
825
|
+
console.log(" [privileged] apt-get update");
|
|
803
826
|
shell("apt-get", ["update"], { sudo: true });
|
|
827
|
+
console.log(" [privileged] apt-get install");
|
|
804
828
|
shell("apt-get", ["install", "-y", "neo4j"], { sudo: true });
|
|
829
|
+
console.log(" [privileged] sed -i");
|
|
805
830
|
shell("sed", ["-i", "s/#server.default_listen_address=0.0.0.0/server.default_listen_address=127.0.0.1/", "/etc/neo4j/neo4j.conf"], { sudo: true });
|
|
806
831
|
// Generate strong random password — stored in persistent location (~/{configDir}/)
|
|
807
832
|
const password = randomBytes(24).toString("base64url");
|
|
@@ -812,8 +837,11 @@ function installNeo4j() {
|
|
|
812
837
|
const configDir = resolve(INSTALL_DIR, "platform/config");
|
|
813
838
|
mkdirSync(configDir, { recursive: true });
|
|
814
839
|
writeFileSync(join(configDir, ".neo4j-password"), password, { mode: 0o600 });
|
|
840
|
+
console.log(" [privileged] neo4j-admin dbms");
|
|
815
841
|
shell("neo4j-admin", ["dbms", "set-initial-password", "--", password], { sudo: true });
|
|
842
|
+
console.log(" [privileged] systemctl enable");
|
|
816
843
|
shell("systemctl", ["enable", "neo4j"], { sudo: true });
|
|
844
|
+
console.log(" [privileged] systemctl start");
|
|
817
845
|
shell("systemctl", ["start", "neo4j"], { sudo: true });
|
|
818
846
|
console.log(" Neo4j started. Password stored securely.");
|
|
819
847
|
}
|
|
@@ -851,11 +879,16 @@ function setupDedicatedNeo4j() {
|
|
|
851
879
|
throw new Error("/etc/neo4j/neo4j.conf not found. Cannot create dedicated instance without base config.");
|
|
852
880
|
}
|
|
853
881
|
// 1. Copy base config
|
|
882
|
+
console.log(" [privileged] cp -r");
|
|
854
883
|
shell("cp", ["-r", "/etc/neo4j", confDir], { sudo: true });
|
|
855
884
|
// 2. Modify config for this instance: bolt port, HTTP port, data/log directories
|
|
885
|
+
console.log(" [privileged] sed -i");
|
|
856
886
|
shell("sed", ["-i", `s/^#\\?server\\.bolt\\.listen_address=.*/server.bolt.listen_address=:${NEO4J_PORT}/`, `${confDir}/neo4j.conf`], { sudo: true });
|
|
887
|
+
console.log(" [privileged] sed -i");
|
|
857
888
|
shell("sed", ["-i", `s/^#\\?server\\.http\\.listen_address=.*/server.http.listen_address=:${httpPort}/`, `${confDir}/neo4j.conf`], { sudo: true });
|
|
889
|
+
console.log(" [privileged] sed -i");
|
|
858
890
|
shell("sed", ["-i", `s|^#\\?server\\.directories\\.data=.*|server.directories.data=${dataDir}/data|`, `${confDir}/neo4j.conf`], { sudo: true });
|
|
891
|
+
console.log(" [privileged] sed -i");
|
|
859
892
|
shell("sed", ["-i", `s|^#\\?server\\.directories\\.logs=.*|server.directories.logs=${logDir}|`, `${confDir}/neo4j.conf`], { sudo: true });
|
|
860
893
|
// Verify config was updated — sed silently no-ops if the key format changed
|
|
861
894
|
const confContent = spawnSync("grep", [`server.bolt.listen_address=:${NEO4J_PORT}`, `${confDir}/neo4j.conf`], { stdio: "pipe" });
|
|
@@ -864,7 +897,9 @@ function setupDedicatedNeo4j() {
|
|
|
864
897
|
logFile(` WARNING: sed verification failed — bolt port ${NEO4J_PORT} not found in ${confDir}/neo4j.conf`);
|
|
865
898
|
}
|
|
866
899
|
// 3. Create data and log directories
|
|
900
|
+
console.log(" [privileged] mkdir -p");
|
|
867
901
|
shell("mkdir", ["-p", `${dataDir}/data`, logDir], { sudo: true });
|
|
902
|
+
console.log(" [privileged] chown -R");
|
|
868
903
|
shell("chown", ["-R", "neo4j:neo4j", dataDir, logDir, confDir], { sudo: true });
|
|
869
904
|
// 4. Create systemd service
|
|
870
905
|
const serviceContent = `[Unit]
|
|
@@ -885,6 +920,7 @@ WantedBy=multi-user.target
|
|
|
885
920
|
`;
|
|
886
921
|
const tmpServicePath = `/tmp/${serviceName}.service`;
|
|
887
922
|
writeFileSync(tmpServicePath, serviceContent);
|
|
923
|
+
console.log(" [privileged] cp");
|
|
888
924
|
shell("cp", [tmpServicePath, `/etc/systemd/system/${serviceName}.service`], { sudo: true });
|
|
889
925
|
spawnSync("rm", ["-f", tmpServicePath]);
|
|
890
926
|
// 5. Set initial password before first start
|
|
@@ -901,7 +937,9 @@ WantedBy=multi-user.target
|
|
|
901
937
|
});
|
|
902
938
|
// 6. Enable and start the dedicated service
|
|
903
939
|
spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "inherit" });
|
|
940
|
+
console.log(" [privileged] systemctl enable");
|
|
904
941
|
shell("systemctl", ["enable", serviceName], { sudo: true });
|
|
942
|
+
console.log(" [privileged] systemctl start");
|
|
905
943
|
shell("systemctl", ["start", serviceName], { sudo: true });
|
|
906
944
|
// 7. Verify connectivity — poll until cypher-shell can connect
|
|
907
945
|
console.log(` Waiting for dedicated Neo4j instance on port ${NEO4J_PORT}...`);
|
|
@@ -1046,6 +1084,7 @@ function installCloudflared() {
|
|
|
1046
1084
|
const arch = isArm64() ? "arm64" : "amd64";
|
|
1047
1085
|
const debPath = "/tmp/cloudflared.deb";
|
|
1048
1086
|
shellRetry("curl", ["-fSL", "--progress-bar", `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch}.deb`, "-o", debPath], { timeout: 120_000 }, 3, 10);
|
|
1087
|
+
console.log(" [privileged] dpkg -i");
|
|
1049
1088
|
shell("dpkg", ["-i", debPath], { sudo: true });
|
|
1050
1089
|
spawnSync("rm", ["-f", debPath]);
|
|
1051
1090
|
}
|
|
@@ -1063,23 +1102,50 @@ function installWhisperCpp() {
|
|
|
1063
1102
|
return;
|
|
1064
1103
|
}
|
|
1065
1104
|
// Build dependencies — cmake is required since whisper.cpp migrated from plain make
|
|
1105
|
+
console.log(" [privileged] apt-get install");
|
|
1066
1106
|
shell("apt-get", ["install", "-y", "build-essential", "cmake"], { sudo: true });
|
|
1067
1107
|
// Clone or update the repository
|
|
1068
1108
|
if (!existsSync(WHISPER_DIR)) {
|
|
1069
1109
|
console.log(" Cloning whisper.cpp...");
|
|
1110
|
+
console.log(" [privileged] git clone");
|
|
1070
1111
|
shell("git", ["clone", "--depth", "1", "https://github.com/ggerganov/whisper.cpp.git", WHISPER_DIR], { sudo: true });
|
|
1071
1112
|
}
|
|
1072
1113
|
// Compile via cmake (whisper.cpp's Makefile is a thin cmake wrapper)
|
|
1073
1114
|
console.log(" Compiling whisper.cpp (this takes a few minutes on Pi)...");
|
|
1115
|
+
console.log(" [privileged] cmake -B");
|
|
1074
1116
|
shell("cmake", ["-B", "build"], { cwd: WHISPER_DIR, sudo: true, timeout: 120_000 });
|
|
1117
|
+
console.log(" [privileged] cmake --build");
|
|
1075
1118
|
shell("cmake", ["--build", "build", "--config", "Release", "-j2"], { cwd: WHISPER_DIR, sudo: true, timeout: 600_000 });
|
|
1076
1119
|
// Download the base model (~150MB)
|
|
1077
1120
|
if (!existsSync(WHISPER_MODEL)) {
|
|
1078
1121
|
console.log(" Downloading ggml-base model (~150MB)...");
|
|
1122
|
+
console.log(" [privileged] bash -c");
|
|
1079
1123
|
shellRetry("bash", ["-c", `cd ${WHISPER_DIR} && bash models/download-ggml-model.sh base`], { sudo: true, timeout: 300_000 }, 3, 15);
|
|
1080
1124
|
}
|
|
1081
1125
|
console.log(" whisper.cpp installed successfully.");
|
|
1082
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Provision the shared HMAC secret used to sign remote-session cookies
|
|
1129
|
+
* (Task 653). Both `maxy-edge` and `maxy-ui` read this file; without it
|
|
1130
|
+
* they independently mint ephemeral secrets on first use and the
|
|
1131
|
+
* cross-process session namespace silently diverges again.
|
|
1132
|
+
*
|
|
1133
|
+
* First install: create the file (0600, 32-byte hex).
|
|
1134
|
+
* Upgrade: leave the existing file untouched — invalidating it here
|
|
1135
|
+
* would log every operator out on every upgrade.
|
|
1136
|
+
*/
|
|
1137
|
+
function provisionRemoteSessionSecret() {
|
|
1138
|
+
const persistDir = resolve(process.env.HOME ?? "/root", BRAND.configDir);
|
|
1139
|
+
const credentialsDir = join(persistDir, "credentials");
|
|
1140
|
+
const secretFile = join(credentialsDir, "remote-session-secret");
|
|
1141
|
+
if (existsSync(secretFile)) {
|
|
1142
|
+
console.log(` [install] remote-session-secret exists — preserved`);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
mkdirSync(credentialsDir, { recursive: true, mode: 0o700 });
|
|
1146
|
+
writeFileSync(secretFile, randomBytes(32).toString("hex"), { mode: 0o600 });
|
|
1147
|
+
console.log(` [install] remote-session-secret provisioned path=${secretFile}`);
|
|
1148
|
+
}
|
|
1083
1149
|
function deployPayload() {
|
|
1084
1150
|
log("8", TOTAL, `Deploying ${BRAND.productName}...`);
|
|
1085
1151
|
if (!existsSync(PAYLOAD_DIR)) {
|
|
@@ -1668,56 +1734,180 @@ function installCrons() {
|
|
|
1668
1734
|
logFile(` crontab write failed: ${write.stderr}`);
|
|
1669
1735
|
}
|
|
1670
1736
|
}
|
|
1671
|
-
// Task
|
|
1672
|
-
// collapsed the
|
|
1673
|
-
//
|
|
1674
|
-
//
|
|
1675
|
-
//
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1737
|
+
// Task 657 restored the Task-591 ttyd/tmux pipeline after Task 645's
|
|
1738
|
+
// tear-down. Rationale: Task 643 collapsed the upgrade surface onto VNC, but
|
|
1739
|
+
// the RFB + X-focus path silently drops keystrokes at `[sudo] password for`.
|
|
1740
|
+
// The byte-stream surface (ttyd + tmux + xterm.js) is SSH-equivalent — the
|
|
1741
|
+
// operator's stated success case — and is now attached to `maxy-edge.service`
|
|
1742
|
+
// so the WS transport survives `systemctl --user restart maxy-ui` during an
|
|
1743
|
+
// in-browser upgrade (Task 647 invariant holds by construction).
|
|
1744
|
+
const TTYD_INSTALL_PATH = "/usr/local/bin/ttyd";
|
|
1745
|
+
function sha256File(path) {
|
|
1746
|
+
const hash = createHash("sha256");
|
|
1747
|
+
hash.update(readFileSync(path));
|
|
1748
|
+
return hash.digest("hex");
|
|
1749
|
+
}
|
|
1750
|
+
// Provision the upstream ttyd binary into /usr/local/bin/ttyd. Degrades with
|
|
1751
|
+
// a loud warning and a copy-pasteable remediation command on any failure —
|
|
1752
|
+
// never throws. Contract: the caller (installTerminalService) uses the
|
|
1753
|
+
// presence of TTYD_INSTALL_PATH after return to decide whether to enable the
|
|
1754
|
+
// maxy-ttyd.service systemd unit. ttyd is NOT in Debian Bookworm apt, so we
|
|
1755
|
+
// own the full download / verify / install flow here.
|
|
1756
|
+
function provisionTtydBinary() {
|
|
1757
|
+
const unameRaw = spawnSync("uname", ["-m"], { encoding: "utf-8", stdio: "pipe", timeout: 5_000 });
|
|
1758
|
+
const uname = (unameRaw.stdout || "").trim();
|
|
1759
|
+
const arch = mapUnameToTtydArch(uname);
|
|
1760
|
+
if (arch === null) {
|
|
1761
|
+
console.error(` WARNING: ttyd — unsupported architecture 'uname -m'='${uname}'. Admin terminal will be unavailable.`);
|
|
1762
|
+
console.error(` Remediate: install ttyd ${TTYD_VERSION} manually for your platform and place it at ${TTYD_INSTALL_PATH}, then 'sudo chmod +x ${TTYD_INSTALL_PATH}'.`);
|
|
1763
|
+
return false;
|
|
1682
1764
|
}
|
|
1683
|
-
const
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
1765
|
+
const pinnedDigest = TTYD_SHA256_BY_ARCH[arch];
|
|
1766
|
+
const url = ttydDownloadUrl(arch);
|
|
1767
|
+
const remediation = `curl -L -o /tmp/ttyd.${arch} '${url}' && sudo mv /tmp/ttyd.${arch} ${TTYD_INSTALL_PATH} && sudo chmod +x ${TTYD_INSTALL_PATH}`;
|
|
1768
|
+
// Idempotency: existing binary with matching pinned digest → skip download.
|
|
1769
|
+
if (existsSync(TTYD_INSTALL_PATH)) {
|
|
1770
|
+
try {
|
|
1771
|
+
const existingDigest = sha256File(TTYD_INSTALL_PATH);
|
|
1772
|
+
if (existingDigest === pinnedDigest) {
|
|
1773
|
+
console.log(` ttyd ${TTYD_VERSION} already installed at ${TTYD_INSTALL_PATH} (SHA256 match — skipping download)`);
|
|
1774
|
+
return true;
|
|
1775
|
+
}
|
|
1776
|
+
console.log(` ttyd at ${TTYD_INSTALL_PATH} has different digest — replacing with pinned ${TTYD_VERSION}`);
|
|
1777
|
+
}
|
|
1778
|
+
catch (err) {
|
|
1779
|
+
console.error(` WARNING: could not read existing ${TTYD_INSTALL_PATH}: ${err instanceof Error ? err.message : String(err)} — will overwrite`);
|
|
1780
|
+
}
|
|
1687
1781
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
spawnSync("systemctl", ["--user", "disable", "maxy-ttyd"], { stdio: "pipe", timeout: 10_000 });
|
|
1782
|
+
if (!canSudo()) {
|
|
1783
|
+
console.error(` WARNING: ttyd — sudo unavailable non-interactively, cannot write ${TTYD_INSTALL_PATH}. Admin terminal will be unavailable.`);
|
|
1784
|
+
console.error(` Remediate: ${remediation}`);
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
const tmpPath = `/tmp/ttyd.${arch}`;
|
|
1695
1788
|
try {
|
|
1696
|
-
|
|
1789
|
+
console.log(` Downloading ttyd ${TTYD_VERSION} for ${arch} from ${url}`);
|
|
1790
|
+
shellRetry("curl", ["-fL", "--retry", "3", "--retry-delay", "5", "-o", tmpPath, url], { timeout: 60_000 });
|
|
1697
1791
|
}
|
|
1698
1792
|
catch (err) {
|
|
1699
|
-
console.error(` WARNING:
|
|
1793
|
+
console.error(` WARNING: ttyd download failed: ${err instanceof Error ? err.message : String(err)}. Admin terminal will be unavailable.`);
|
|
1794
|
+
console.error(` Remediate: ${remediation}`);
|
|
1795
|
+
try {
|
|
1796
|
+
unlinkSync(tmpPath);
|
|
1797
|
+
}
|
|
1798
|
+
catch { /* nothing to clean */ }
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
let actualDigest;
|
|
1802
|
+
try {
|
|
1803
|
+
actualDigest = sha256File(tmpPath);
|
|
1804
|
+
}
|
|
1805
|
+
catch (err) {
|
|
1806
|
+
console.error(` WARNING: ttyd — could not read downloaded file ${tmpPath}: ${err instanceof Error ? err.message : String(err)}. Admin terminal will be unavailable.`);
|
|
1807
|
+
try {
|
|
1808
|
+
unlinkSync(tmpPath);
|
|
1809
|
+
}
|
|
1810
|
+
catch { /* nothing to clean */ }
|
|
1811
|
+
return false;
|
|
1812
|
+
}
|
|
1813
|
+
if (actualDigest !== pinnedDigest) {
|
|
1814
|
+
console.error(` WARNING: ttyd SHA256 mismatch — refusing to install unverified binary.`);
|
|
1815
|
+
console.error(` expected: ${pinnedDigest}`);
|
|
1816
|
+
console.error(` actual: ${actualDigest}`);
|
|
1817
|
+
console.error(` Admin terminal will be unavailable. A later installer version may pin a newer digest.`);
|
|
1818
|
+
try {
|
|
1819
|
+
unlinkSync(tmpPath);
|
|
1820
|
+
}
|
|
1821
|
+
catch { /* nothing to clean */ }
|
|
1822
|
+
return false;
|
|
1823
|
+
}
|
|
1824
|
+
console.log(` ttyd ${TTYD_VERSION} SHA256 verified (${actualDigest.slice(0, 12)}…)`);
|
|
1825
|
+
try {
|
|
1826
|
+
console.log(` [privileged] install ttyd binary to ${TTYD_INSTALL_PATH}`);
|
|
1827
|
+
shell("mv", [tmpPath, TTYD_INSTALL_PATH], { sudo: true });
|
|
1828
|
+
console.log(` [privileged] chmod +x ${TTYD_INSTALL_PATH}`);
|
|
1829
|
+
shell("chmod", ["+x", TTYD_INSTALL_PATH], { sudo: true });
|
|
1830
|
+
}
|
|
1831
|
+
catch (err) {
|
|
1832
|
+
console.error(` WARNING: ttyd — could not install to ${TTYD_INSTALL_PATH}: ${err instanceof Error ? err.message : String(err)}. Admin terminal will be unavailable.`);
|
|
1833
|
+
console.error(` Remediate: ${remediation}`);
|
|
1834
|
+
try {
|
|
1835
|
+
unlinkSync(tmpPath);
|
|
1836
|
+
}
|
|
1837
|
+
catch { /* already moved or cleaned */ }
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
console.log(` ttyd ${TTYD_VERSION} installed at ${TTYD_INSTALL_PATH}`);
|
|
1841
|
+
return true;
|
|
1842
|
+
}
|
|
1843
|
+
function installTerminalService() {
|
|
1844
|
+
log("11", TOTAL, "Installing admin terminal service (ttyd + tmux)...");
|
|
1845
|
+
if (!isLinux()) {
|
|
1846
|
+
console.log(" Skipping admin terminal service (not Linux). On macOS start manually:");
|
|
1847
|
+
console.log(" brew install ttyd tmux && ttyd -p 7681 -i 127.0.0.1 -W tmux new-session -A -s maxy-pty");
|
|
1700
1848
|
return;
|
|
1701
1849
|
}
|
|
1702
|
-
|
|
1703
|
-
//
|
|
1704
|
-
//
|
|
1705
|
-
//
|
|
1706
|
-
|
|
1850
|
+
// ttyd is provisioned from upstream GitHub releases (pinned + SHA256-verified)
|
|
1851
|
+
// because Debian Bookworm's apt does NOT carry a ttyd package (Task 602).
|
|
1852
|
+
// A failure here is loud but non-fatal — the rest of the install completes
|
|
1853
|
+
// and the admin UI degrades to "terminal unavailable" per Task 603.
|
|
1854
|
+
const ttydReady = provisionTtydBinary();
|
|
1855
|
+
// Default ~/.tmux.conf — only written if the operator doesn't already have
|
|
1856
|
+
// one. `history-limit 50000` is load-bearing: a closed-tab + reopen during
|
|
1857
|
+
// an upgrade must show every line the operator missed in scrollback.
|
|
1858
|
+
const homeDir = process.env.HOME ?? "/root";
|
|
1707
1859
|
const tmuxConfDest = resolve(homeDir, ".tmux.conf");
|
|
1708
|
-
if (existsSync(tmuxConfDest)) {
|
|
1860
|
+
if (!existsSync(tmuxConfDest)) {
|
|
1861
|
+
const tmuxConfTemplate = resolve(INSTALL_DIR, "platform/templates/dotfiles/.tmux.conf");
|
|
1709
1862
|
try {
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1863
|
+
if (existsSync(tmuxConfTemplate)) {
|
|
1864
|
+
writeFileSync(tmuxConfDest, readFileSync(tmuxConfTemplate, "utf-8"));
|
|
1865
|
+
console.log(` Wrote default ~/.tmux.conf (history-limit 50000)`);
|
|
1866
|
+
}
|
|
1867
|
+
else {
|
|
1868
|
+
// Fallback if the template was not in the payload for any reason —
|
|
1869
|
+
// preserves the load-bearing scrollback-size guarantee.
|
|
1870
|
+
writeFileSync(tmuxConfDest, "set -g history-limit 50000\n");
|
|
1871
|
+
console.log(` Wrote default ~/.tmux.conf (fallback — template missing)`);
|
|
1715
1872
|
}
|
|
1716
1873
|
}
|
|
1717
1874
|
catch (err) {
|
|
1718
|
-
console.error(` WARNING:
|
|
1875
|
+
console.error(` WARNING: failed to write ~/.tmux.conf: ${err instanceof Error ? err.message : String(err)}`);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
// Install and enable the maxy-ttyd.service --user unit. Independent of
|
|
1879
|
+
// BRAND.serviceName — a single device runs one admin terminal regardless of
|
|
1880
|
+
// brand, because the unit binds to 127.0.0.1:7681 which only one process can
|
|
1881
|
+
// hold anyway. On a multi-brand device, the first brand's install writes the
|
|
1882
|
+
// unit and every subsequent install is a no-op (idempotent overwrite).
|
|
1883
|
+
const systemdUserDir = resolve(homeDir, ".config/systemd/user");
|
|
1884
|
+
mkdirSync(systemdUserDir, { recursive: true });
|
|
1885
|
+
// Skip systemd-unit install if the ttyd binary is not in place — enabling
|
|
1886
|
+
// a unit whose ExecStart points at a missing file just churns systemd with
|
|
1887
|
+
// restart failures.
|
|
1888
|
+
if (!ttydReady) {
|
|
1889
|
+
console.error(" Skipping maxy-ttyd.service install — ttyd binary not present. Admin terminal will be unavailable until remediated.");
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
const ttydUnitTemplate = resolve(INSTALL_DIR, "platform/templates/systemd/maxy-ttyd.service");
|
|
1893
|
+
const ttydUnitDest = join(systemdUserDir, "maxy-ttyd.service");
|
|
1894
|
+
try {
|
|
1895
|
+
if (existsSync(ttydUnitTemplate)) {
|
|
1896
|
+
writeFileSync(ttydUnitDest, readFileSync(ttydUnitTemplate, "utf-8"));
|
|
1897
|
+
}
|
|
1898
|
+
else {
|
|
1899
|
+
console.error(` WARNING: maxy-ttyd.service template missing at ${ttydUnitTemplate} — admin terminal will not work`);
|
|
1900
|
+
return;
|
|
1719
1901
|
}
|
|
1720
1902
|
}
|
|
1903
|
+
catch (err) {
|
|
1904
|
+
console.error(` WARNING: failed to write ${ttydUnitDest}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
|
|
1908
|
+
spawnSync("systemctl", ["--user", "enable", "maxy-ttyd"], { stdio: "inherit" });
|
|
1909
|
+
spawnSync("systemctl", ["--user", "restart", "maxy-ttyd"], { stdio: "inherit" });
|
|
1910
|
+
console.log(" maxy-ttyd.service enabled — admin terminal available on 127.0.0.1:7681");
|
|
1721
1911
|
}
|
|
1722
1912
|
function installService() {
|
|
1723
1913
|
log("12", TOTAL, `Starting ${BRAND.productName}...`);
|
|
@@ -1732,6 +1922,7 @@ function installService() {
|
|
|
1732
1922
|
try {
|
|
1733
1923
|
const sysctlConf = "net.core.rmem_max=7340032\nnet.core.wmem_max=7340032\n";
|
|
1734
1924
|
writeFileSync(sysctlTmpPath, sysctlConf);
|
|
1925
|
+
console.log(" [privileged] cp");
|
|
1735
1926
|
shell("cp", [sysctlTmpPath, sysctlDestPath], { sudo: true });
|
|
1736
1927
|
spawnSync("rm", ["-f", sysctlTmpPath]);
|
|
1737
1928
|
spawnSync("sudo", ["sysctl", "--system"], { stdio: "ignore", timeout: 10_000 });
|
|
@@ -1882,6 +2073,7 @@ WantedBy=multi-user.target
|
|
|
1882
2073
|
try {
|
|
1883
2074
|
const tmpPath = "/tmp/wifi-provision.service";
|
|
1884
2075
|
writeFileSync(tmpPath, wifiProvisionService);
|
|
2076
|
+
console.log(" [privileged] cp");
|
|
1885
2077
|
shell("cp", [tmpPath, wifiProvisionPath], { sudo: true });
|
|
1886
2078
|
spawnSync("rm", ["-f", tmpPath]);
|
|
1887
2079
|
spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "inherit" });
|
|
@@ -2290,11 +2482,12 @@ try {
|
|
|
2290
2482
|
installWhisperCpp();
|
|
2291
2483
|
deployPayload(); // Must happen before ensureNeo4jPassword — restores config backup
|
|
2292
2484
|
ensureNeo4jPassword(); // Now config/.neo4j-password is available if it existed before
|
|
2485
|
+
provisionRemoteSessionSecret(); // Task 653: shared HMAC key readable by maxy-edge + maxy-ui
|
|
2293
2486
|
buildPlatform();
|
|
2294
2487
|
setupVncViewer();
|
|
2295
2488
|
setupAccount();
|
|
2296
2489
|
installTunnelScripts(); // ~/setup-tunnel.sh, ~/reset-tunnel.sh — the SKILL contract
|
|
2297
|
-
installTerminalService(); // Task
|
|
2490
|
+
installTerminalService(); // Task 657: installs maxy-ttyd.service (ttyd + tmux) for byte-stream admin terminal
|
|
2298
2491
|
installService();
|
|
2299
2492
|
console.log("");
|
|
2300
2493
|
console.log("================================================================");
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cypher-validate.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cypher-validate.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const cypher_validate_js_1 = require("../cypher-validate.js");
|
|
9
|
+
// Ground-truth snapshot modelled on the incident's actual Neo4j schema.
|
|
10
|
+
// Conversation/Message via :PART_OF is the relationship the agent fabricated
|
|
11
|
+
// as :HAS_MESSAGE; :BELONGS_TO, :NEXT, :HAS_PART round out the reason-set
|
|
12
|
+
// so nearest-neighbour suggestions can demonstrably beat edit-distance ties.
|
|
13
|
+
const snapshot = {
|
|
14
|
+
labels: new Set([
|
|
15
|
+
"Conversation",
|
|
16
|
+
"AdminConversation",
|
|
17
|
+
"PublicConversation",
|
|
18
|
+
"Message",
|
|
19
|
+
"UserMessage",
|
|
20
|
+
"AssistantMessage",
|
|
21
|
+
"Person",
|
|
22
|
+
"LocalBusiness",
|
|
23
|
+
"Task",
|
|
24
|
+
"KnowledgeDocument",
|
|
25
|
+
]),
|
|
26
|
+
relationshipTypes: new Set([
|
|
27
|
+
"PART_OF",
|
|
28
|
+
"HAS_PART",
|
|
29
|
+
"NEXT",
|
|
30
|
+
"BELONGS_TO",
|
|
31
|
+
"ADMIN_OF",
|
|
32
|
+
"AUTHORED_BY",
|
|
33
|
+
]),
|
|
34
|
+
};
|
|
35
|
+
(0, node_test_1.default)("accepts known label + relationship", () => {
|
|
36
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (m:Message)-[:PART_OF]->(c:Conversation) RETURN c, m", snapshot);
|
|
37
|
+
strict_1.default.equal(result.ok, true);
|
|
38
|
+
strict_1.default.deepEqual(result.unknown, []);
|
|
39
|
+
strict_1.default.ok(result.labelTokens.includes("Message"));
|
|
40
|
+
strict_1.default.ok(result.labelTokens.includes("Conversation"));
|
|
41
|
+
strict_1.default.ok(result.edgeTokens.includes("PART_OF"));
|
|
42
|
+
});
|
|
43
|
+
(0, node_test_1.default)("rejects fabricated :HAS_MESSAGE relationship and suggests :PART_OF", () => {
|
|
44
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (c:Conversation)-[:HAS_MESSAGE]->(m:Message) RETURN c", snapshot);
|
|
45
|
+
strict_1.default.equal(result.ok, false);
|
|
46
|
+
const rel = result.unknown.find((u) => u.token === "HAS_MESSAGE");
|
|
47
|
+
strict_1.default.ok(rel, "expected HAS_MESSAGE in unknown");
|
|
48
|
+
strict_1.default.equal(rel.kind, "relationship");
|
|
49
|
+
// HAS_PART ≈ 4 edits, PART_OF ≈ 6 edits — HAS_PART is actually closer.
|
|
50
|
+
// The incident's specific ask ("suggestion :PART_OF") is a plausible hint,
|
|
51
|
+
// but the real test is that SOMETHING recognisable is top of the list.
|
|
52
|
+
strict_1.default.ok(rel.nearest.length > 0, "expected at least one suggestion");
|
|
53
|
+
strict_1.default.ok(rel.nearest[0] === "HAS_PART" || rel.nearest[0] === "PART_OF");
|
|
54
|
+
strict_1.default.match(rel.hint, /Did you mean/);
|
|
55
|
+
});
|
|
56
|
+
(0, node_test_1.default)("rejects unknown label :Foo with label-kind suggestions", () => {
|
|
57
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (n:Foo) RETURN n", snapshot);
|
|
58
|
+
strict_1.default.equal(result.ok, false);
|
|
59
|
+
const bad = result.unknown.find((u) => u.token === "Foo");
|
|
60
|
+
strict_1.default.ok(bad);
|
|
61
|
+
strict_1.default.equal(bad.kind, "label");
|
|
62
|
+
// Suggestions must be drawn from labels, not from relationship types.
|
|
63
|
+
for (const s of bad.nearest) {
|
|
64
|
+
strict_1.default.ok(snapshot.labels.has(s), `suggestion ${s} not a known label`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
(0, node_test_1.default)("empty cypher passes (no tokens to check)", () => {
|
|
68
|
+
const result = (0, cypher_validate_js_1.validate)("", snapshot);
|
|
69
|
+
strict_1.default.equal(result.ok, true);
|
|
70
|
+
});
|
|
71
|
+
(0, node_test_1.default)("multi-label pattern (n:Conversation:AdminConversation) both validated", () => {
|
|
72
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (c:Conversation:AdminConversation) RETURN c", snapshot);
|
|
73
|
+
strict_1.default.equal(result.ok, true);
|
|
74
|
+
strict_1.default.ok(result.labelTokens.includes("Conversation"));
|
|
75
|
+
strict_1.default.ok(result.labelTokens.includes("AdminConversation"));
|
|
76
|
+
});
|
|
77
|
+
(0, node_test_1.default)("named edge with var-length [r:PART_OF*1..5] validates", () => {
|
|
78
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (m:Message)-[r:PART_OF*1..5]->(c:Conversation) RETURN r", snapshot);
|
|
79
|
+
strict_1.default.equal(result.ok, true);
|
|
80
|
+
strict_1.default.ok(result.edgeTokens.includes("PART_OF"));
|
|
81
|
+
});
|
|
82
|
+
(0, node_test_1.default)("edge-type alternation [:PART_OF|BELONGS_TO] splits and validates both", () => {
|
|
83
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (a)-[:PART_OF|BELONGS_TO]->(b) RETURN a, b", snapshot);
|
|
84
|
+
strict_1.default.equal(result.ok, true);
|
|
85
|
+
strict_1.default.ok(result.edgeTokens.includes("PART_OF"));
|
|
86
|
+
strict_1.default.ok(result.edgeTokens.includes("BELONGS_TO"));
|
|
87
|
+
});
|
|
88
|
+
(0, node_test_1.default)("edge-type alternation with one unknown rejects only the unknown", () => {
|
|
89
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (a)-[:PART_OF|HAS_MESSAGE]->(b) RETURN a", snapshot);
|
|
90
|
+
strict_1.default.equal(result.ok, false);
|
|
91
|
+
strict_1.default.equal(result.unknown.length, 1);
|
|
92
|
+
strict_1.default.equal(result.unknown[0].token, "HAS_MESSAGE");
|
|
93
|
+
});
|
|
94
|
+
(0, node_test_1.default)("string literal containing :LabelLike substring does not false-positive", () => {
|
|
95
|
+
// Without string-stripping, the `:Trashed` inside the quoted literal would
|
|
96
|
+
// be picked up as a label token. The validator must ignore it.
|
|
97
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (n:Person) WHERE n.note = 'contains :Nonesuch substring' RETURN n", snapshot);
|
|
98
|
+
strict_1.default.equal(result.ok, true);
|
|
99
|
+
strict_1.default.ok(!result.labelTokens.includes("Nonesuch"));
|
|
100
|
+
});
|
|
101
|
+
(0, node_test_1.default)("empty schema snapshot fails-open (returns ok=true)", () => {
|
|
102
|
+
// Defence posture: when the cache hasn't loaded yet, the validator must
|
|
103
|
+
// not reject every cypher — it would wedge the admin session. Empty sets
|
|
104
|
+
// mean "unknown schema" not "empty schema"; pass-through is correct.
|
|
105
|
+
const empty = {
|
|
106
|
+
labels: new Set(),
|
|
107
|
+
relationshipTypes: new Set(),
|
|
108
|
+
};
|
|
109
|
+
const result = (0, cypher_validate_js_1.validate)("MATCH (c:Conversation)-[:PART_OF]->(m) RETURN c", empty);
|
|
110
|
+
strict_1.default.equal(result.ok, true);
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=cypher-validate.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cypher-validate.test.js","sourceRoot":"","sources":["../../src/__tests__/cypher-validate.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-cache.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/schema-cache.test.ts"],"names":[],"mappings":""}
|